﻿using System;

using Microsoft.Xna.Framework;

namespace SpaceAdventureLib.Universe
{
    [Serializable]
    public class Camera : NonCollidable
    {

        #region "Properties"

        public enum ViewType
        {
            Free,
            FirstPerson,
            ThirdPerson
        };

        private ViewType viewingType = ViewType.ThirdPerson;
        public ViewType ViewingType
        {
            get { return this.viewingType; }
        }

        private Entity chaser = null;
        public Entity Chaser
        {
            get { return this.chaser; }
        }

        private bool isChasing = false;
        public bool IsChasing
        {
            get { return this.isChasing; }
        }

        private Vector3 chaseLocation = Vector3.Zero;
        /// <summary>
        /// Position of object being chased.
        /// </summary>
        public Vector3 ChaseLocation
        {
            get { return this.chaseLocation; }
        }

        private Vector3 chaseDirection = Vector3.Zero;
        /// <summary>
        /// Direction the chased object is facing.
        /// </summary>
        public Vector3 ChaseDirection
        {
            get { return this.chaseDirection; }
        }
        
        private Vector3 chaseUp = Vector3.Up;
        /// <summary>
        /// Chased object's Up vector.
        /// </summary>
        public Vector3 ChaseUp
        {
            get { return this.chaseUp; }
        }

        private Vector3 desiredPositionOffset = Vector3.Zero;
        /// <summary>
        /// Desired camera position in the chased object's coordinate system.
        /// </summary>
        public Vector3 DesiredPositionOffset
        {
            get { return desiredPositionOffset; }
            set { desiredPositionOffset = value; }
        }
        
        
        #endregion

        #region "Physics Properties"

        private bool usePhysics = false;
        public bool UsePhysics
        {
            get { return this.usePhysics; }
            set { this.usePhysics = value; }
        }

        private Vector3 desiredLocation;
        /// <summary>
        /// Desired camera position in world space.
        /// </summary>
        public Vector3 DesiredLocation
        {
            get { return this.desiredLocation; }
        }

        private float stiffness = 0.0f;
        /// <summary>
        /// Physics coefficient which controls the influence of the camera's position
        /// over the spring force. The stiffer the spring, the closer it will stay to
        /// the chased object.
        /// </summary>
        public float Stiffness
        {
            get { return this.stiffness; }
            set { this.stiffness = value; }
        }

        private float damping = 0.0f;
        /// <summary>
        /// Physics coefficient which approximates internal friction of the spring.
        /// Sufficient damping will prevent the spring from oscillating infinitely.
        /// </summary>
        public float Damping
        {
            get { return this.damping; }
            set { this.damping = value; }
        }

        #endregion

        #region "Perspective properties"

        private float aspectRatio = 4.0f / 3.0f;
        /// <summary>
        /// Perspective aspect ratio. Default value should be overriden by application.
        /// </summary>
        public float AspectRatio
        {
            get { return this.aspectRatio; }
            set 
            { 
                this.aspectRatio = value;
                this.recalcProjection = true;
            }
        }
        
        private float fieldOfView = MathHelper.ToRadians(45.0f);
        /// <summary>
        /// Perspective field of view.
        /// </summary>
        public float FieldOfView
        {
            get { return this.fieldOfView; }
            set 
            { 
                this.fieldOfView = value;
                this.recalcProjection = true;
            }
        }
        
        private float nearPlaneDistance = 1.0f;
        /// <summary>
        /// Distance to the near clipping plane.
        /// </summary>
        public float NearPlaneDistance
        {
            get { return this.nearPlaneDistance; }
            set 
            {
                this.nearPlaneDistance = value;
                this.recalcProjection = true;
            }
        }
        
        private float farPlaneDistance = 100000.0f;
        /// <summary>
        /// Distance to the far clipping plane.
        /// </summary>
        public float FarPlaneDistance
        {
            get { return this.farPlaneDistance; }
            set 
            {
                this.farPlaneDistance = value;
                this.recalcProjection = true;
            }
        }

        private Vector3 lookAtOffset = new Vector3(0, 2.8f, 0);
        /// <summary>
        /// Look at point in the chased object's coordinate system.
        /// </summary>
        public Vector3 LookAtOffset
        {
            get { return lookAtOffset; }
            set { lookAtOffset = value; }
        }
        
        private Vector3 lookAt = Vector3.Zero;
        /// <summary>
        /// Look at point in world space.
        /// </summary>
        public Vector3 LookAt
        {
            get { return this.lookAt; }
        }

        #endregion

        #region "Matrix properties"

        private Matrix view = Matrix.Identity;
        /// <summary>
        /// View transform matrix.
        /// </summary>
        public Matrix View
        {
            get { return view; }
        }

        private Matrix projection = Matrix.Identity;
        /// <summary>
        /// Projecton transform matrix.
        /// </summary>
        public Matrix Projection
        {
            get { return projection; }
        }
        
        #endregion

        #region "Variables"

        [NonSerialized]
        private bool recalcProjection = true;

        #endregion

        #region "Constructor"

        public Camera(Space space)
        {

            this.PreInitialize(space);

            this.UsePhysics = true;
            this.Stiffness = 1800.0f;
            this.Damping = 600.0f;
            this.Mass = 50.0f;

            this.desiredPositionOffset = new Vector3(0, 2.0f, 2.0f);

        }

        #endregion

        #region "Methods"

        public override void FinaliseInitialization()
        {
            // do nothing
        }

        public override void Update(GameTime gameTime)
        {

            float dt = (float)gameTime.ElapsedGameTime.TotalSeconds;

            Vector3 acceleration;

            if (this.usePhysics && (this.Mass > 0))
            {
                Vector3 stretch = this.Location - desiredLocation;
                Vector3 force = -this.stiffness * stretch - this.damping * this.Velocity;
                acceleration = force / this.Mass;
            }
            else
            {
                acceleration = this.Acceleration;
            }
            this.Velocity += acceleration * dt;
            this.RotationVelocity += this.RotationAcceleration * dt;

            this.UpdateDirections();
            this.UpdateChaseData();

            this.UpdateWorldLocations();

            if (this.viewingType == ViewType.ThirdPerson) 
            {
                this.Velocity = Vector3.Zero; // Stop motion            
                this.MoveTo(this.DesiredLocation); // Force desired position
            }

            this.UpdateMatrices();

        }

        private void UpdateChaseData()
        {
            if (this.chaser != null)
            {
                this.chaseDirection = this.chaser.Forward;
                this.chaseLocation = this.chaser.Location;
                this.chaseUp = this.chaser.Up;
            }
        }

        private void UpdateWorldLocations()
        {

            // Construct a matrix to transform from object space to worldspace
            Matrix transform = Matrix.Identity;
            transform.Forward = this.chaseDirection;
            transform.Up = this.chaseUp;
            transform.Right = Vector3.Cross(this.chaseUp, this.chaseDirection);

            // Calculate desired camera properties in world space
            if (this.chaser != null)
            {
                this.desiredLocation = this.chaseLocation + Vector3.TransformNormal(this.desiredPositionOffset, transform);
                this.lookAt = this.chaseLocation + Vector3.TransformNormal(this.lookAtOffset, transform);
            }

        }

        private void UpdateMatrices(bool recalculateProjection)
        {
            view = Matrix.CreateLookAt(this.Location, this.LookAt, this.chaseUp);
            if (recalculateProjection)
            {
                projection = Matrix.CreatePerspectiveFieldOfView(this.fieldOfView, this.aspectRatio, this.nearPlaneDistance, this.farPlaneDistance);
                this.recalcProjection = false;
            }
        }

        private void UpdateMatrices()
        {
            this.UpdateMatrices(this.recalcProjection);            
        }

        public void Reset()
        {

            this.UpdateWorldLocations();            
            this.Velocity = Vector3.Zero; // Stop motion            
            this.MoveTo(this.DesiredLocation); // Force desired position
            this.aspectRatio = this.Space.Mission.Campaign.Game.AspectRatio;
            this.UpdateMatrices(true);

        }

        public void ChangeView(ViewType viewing, Entity chaser)
        {

            this.viewingType = viewing;
            this.chaser = chaser;

            switch (this.viewingType)
            {

                case ViewType.FirstPerson:

                    // add stuff
                    break;

                case ViewType.ThirdPerson:

                    this.UpdateChaseData();
                    this.Reset();
                    break;

            }

            this.UpdateMatrices();

        }

        public override void Draw(Microsoft.Xna.Framework.Graphics.GraphicsDevice graphics)
        {
            // do nothing
        }


        #endregion

    }
}